<?php
class Slash_CalDAV_Server extends Sabre_DAV_Server
{
	/*
	 * This server differs from the core server in that:
	 * -	It can be configured
	 * - 	It is a Singleton
	 * -	It handles getAllowedMethods (OPTIONS) differently
	 * - 	It provides methods for getting user information from the
	 * 		Auth backend.
	 *   
   * Copyright © 2010 Jack Cleaver
   * 
   * This plugin provides functionality added by CalDAV (RFC 4791)
   * It implements new reports, and the MKCALENDAR method.
   *
   * This file is part of Slash.
   *
   * Slash is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * Slash is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with Slash.  If not, see <http://www.gnu.org/licenses/>.
	 */

	private static $instance;

	private static $rootDirectory;
	private static $principalRoot;
	private static $calendarRoot;

	public static function getInstance()
	{
		if (!self::$instance)
		{
			self::$instance = new self();
		}
		return self::$instance;
	}

	function __construct()
	{
		$this->httpResponse = new Sabre_HTTP_Response();
		$this->httpRequest = new Sabre_HTTP_Request();
		$this->setBaseUri($this->guessBaseUri());
	}

	public function configure(Slash_Config $config)
	{
		Slash_Debug::debug("Configuring CalDAV Server");
		self::$rootDirectory = $config->getRootDirectory();
		$rootNode = new Slash_CalDAV_FSExt_Directory($config->getRootDirectory());
		Slash_Debug::debug("Root node constructed");
		$this->tree = new Sabre_DAV_ObjectTree($rootNode);
		Slash_Debug::debug("ObjectTree constructed");
		self::$principalRoot = $config->getPrincipalRoot();
		self::$calendarRoot = $config->getCalendarRoot();
		try
		{
			$junk = $this->tree->getNodeForPath(self::$calendarRoot);
			Slash_Debug::debug("Calendar root collection found");
		}
		catch (Sabre_DAV_Exception_FileNotFound $e)
		{
			try
			{
				Slash_Debug::debug("No calendar root collection found, creating");
				$rootNode->createDirectory($config->getCalendarRoot());
				Slash_Debug::debug("Created calendar root collection");
				$calendarRoot = $calRootNode->getChild($config->getCalendarRoot());
				
				$principalsAcl = new Slash_ACL_Property_Acl("Calendars ACL");
				$princ = new Slash_ACL_Property_PropertyPrincipal("DAV:", "owner");
				$principalsAcl->grant($princ, array(0=>"all"), true);
		
				$princ = new Slash_ACL_Property_HrefPrincipal("/principals/admins");
				$principalsAcl->grant($princ, array(0=>"all"), true);
				$owner = new Slash_ACL_Property_Owner(new Slash_ACL_Property_Href($princ->getHref()));
				
				$princ = new Slash_ACL_AuthenticatedPrincipal();
				$principalsAcl->grant($princ, array(0=>"all"), true);
				
				$propreq = array();
				$propreq["{DAV:}acl"] = $principalsAcl;
				$propreq["{DAV:}owner"] = $owner;
				Slash_Debug::debug("Setting ACL and Owner on principals collection");
				$this->principalRoot->updateProperties($propreq);
				Slash_Debug::debug("Calendar root collection configured in server");
			}
			catch (Exception $e)
			{
				Slash_Debug::debug("Exception configuring calendar root: " . $e->getMessage());
			}
		}
		$this->setBaseUri($config->getBaseUri());
		Slash_Debug::debug("Configured CalDAV Server");
	}



	public function getAllowedMethods($uri)
	{
		try
		{
			$this->tree->getNodeForPath($uri);
			$exists = true;
		}
		catch (Sabre_DAV_Exception_FileNotFound $e)
		{
			$exists = false;
		}
		//Slash_Debug::debug("CalDAV_Server getAllowedMethods");
		$methods = array('OPTIONS');
		$readMethods = array('GET','HEAD','PROPFIND','REPORT');
		$writeMethods = array('DELETE','PROPPATCH','COPY','MOVE');

		switch ($uri)
		{
			case "":
				$methods = array_merge($methods, $readMethods);
				break;
			case "*":
				$methods = array_merge($methods, $readMethods);
				$methods = array_merge($methods, $writeMethods);
				$methods[] = "PUT";
				break;
			default:
				if ($exists)
				{
					$methods = array_merge($methods, $readMethods);
					$methods = array_merge($methods, $writeMethods);
				}
				$methods[] = "PUT";
				// The MKCOL is only allowed on an unmapped uri
				try
				{
					$node = $this->tree->getNodeForPath($uri);
				}
				catch (Sabre_DAV_Exception_FileNotFound $e)
				{
					$methods[] = 'MKCOL';
				}
		}

		// We're also checking if any of the plugins register any new methods
		foreach($this->plugins as $plugin)
		{
			$methods = array_merge($methods,$plugin->getHTTPMethods($uri));
		}
		array_unique($methods);
		//Slash_Debug::debugDump("CalDAV_Server getAllowedMethods for $uri", $methods);
		return $methods;
	}

	public static function getCalendarRoot()
	{
		return self::$calendarRoot;
	}

	public static function getRootDirectory()
	{
		return self::$rootDirectory;
	}
	
	public static function getPrincipalRoot()
	{
		return self::$principalRoot;
	}

	public function createCollection($uri, array $resourceType, array $properties)
	{
    list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri);
		$parent = $this->tree->getNodeForPath($parentUri);
		Slash_Debug::debug("CalDAV_Server createCollection: parent $parentUri is a " . get_class($parent));
		return parent::createCollection($uri, $resourceType, $properties);
	}



	/*
	 * Update properties only if none are protected properties.
	 * acl is updated by ACL method.
	 * owner is not protected, in the sense that it is in the protectedProperties array;
	 * but it gets special handling here.
	 * Only admins and the owner can change the owner.
	 * Hmmm. But owner is an ACL property; so it should be handled by the ACL plugin.
	 * But the ACL plugin doesn't get a beforeUpdateProperties event. So give it one!
	 */
	public function updateProperties($uri, array $properties)
	{
		Slash_Debug::debug("Slash_CalDAV_Server updateProperties");
		$node = $this->tree->getNodeForPath($uri);
			
		$result = array(
			200 => array(),
			403 => array(),
			424 => array(),
		);
		$remainingProperties = $properties;
		$hasError = false;
		if (!$this->broadcastEvent('beforeUpdateProperties', array($uri, $properties, &$result)))
		{
			return $result;
		}


		// If the node is not an instance of Sabre_DAV_IProperties, every
		// property is 403 Forbidden
		// simply return a 405.
		if (!($node instanceof Sabre_DAV_IProperties))
		{
			Slash_Debug::debug("CalDAV_Server updateProperties: URI is not an IProperties");
			$hasError = true;
			foreach($properties as $propertyName => $value)
			{
				$result[403][$propertyName] = null;
			}
			$remainingProperties = array();
		}

		// Running through all properties to make sure none of them are protected
		if (!$hasError)
		{
			foreach($properties as $propertyName => $value)
			{
				if(in_array($propertyName, $this->protectedProperties))
				{
					Slash_Debug::debug("CalDAV_Server updateProperties: Property is protected");
					$result[403][$propertyName] = null;
					unset($remainingProperties[$propertyName]);
					$hasError = true;
				}
			}
		}

		// Only if there were no errors we may attempt to update the resource
		if (!$hasError)
		{
			Slash_Debug::debug("CalDAV_Server updateProperties: asking node to update");
			$updateResult = $node->updateProperties($properties);
			$remainingProperties = array();

			if ($updateResult===true)
			{
				Slash_Debug::debug("CalDAV_Server updateProperties: update succeeded");
				foreach($properties as $propertyName=>$value)
				{
					$result[200][$propertyName] = null;
				}
			} elseif ($updateResult===false)
			{
				Slash_Debug::debug("CalDAV_Server updateProperties: update failed");
				foreach($properties as $propertyName=>$value)
				{
					$result[403][$propertyName] = null;
				}
			} elseif (is_array($updateResult))
			{
				// The node has detailed update information
				Slash_Debug::debug("CalDAV_Server updateProperties: update multistatus");
				$result = $updateResult;
			} else {
				throw new Sabre_DAV_Exception('Invalid result from updateProperties');
			}
		}
		foreach($remainingProperties as $propertyName=>$value)
		{
			// if there are remaining properties, it must mean
			// there's a dependency failure
			Slash_Debug::debug("CalDAV_Server updateProperties: dependency failure");
			$result[424][$propertyName] = null;
		}
		foreach($result as $status=>$props)
		{
			if (count($props)===0)
			{
				unset($result[$status]);
			}
		}
		$result['href'] = $uri;
		Slash_Debug::debugDump("Slash_CalDAV_Server updateProperties", $result);
		return $result;
	}


	//Return an array of Principal resources containing all principals.
	public function getAllPrincipals()
	{
		$principals = array();
		$aclPlugin = $this->getPlugin("Slash_ACL_Plugin");
		if ($aclPlugin)
		{
			$users = $aclPlugin->authBackend->getUsers();
			foreach($users as $user)
			{
				//$parts = explode("/", $user["uri"]);
				//$name = $parts[count($parts)-1];
				$principal = $this->tree->getNodeForPath($user["uri"]);
				$principals[] = $principal;
			}
		}
		return $principals;
	}

	//Return a Principal resource representing the current user.
	public function getCurrentUserPrincipal()
	{
		$principal = null;
		$aclPlugin = $this->getPlugin("Slash_ACL_Plugin");
		if ($aclPlugin)
		{
			$username = $aclPlugin->authBackend->getCurrentUser();
			if ($username)
			{
				$uri = $username["uri"];
				Slash_Debug::debug("CalDAV Server: getCurrentUserPrincipal: uri=$uri");
				try
				{
					$principal = $this->tree->getNodeForPath($uri);
				}
				catch (Sabre_DAV_Exception $e)
				{
					$msg = $e->getMessage();
					Slash_Debug::debug("CalDAV Server: getCurrentUserPrincipal: Exception getting node for $uri: $msg");
				}
			} else {
				Slash_Debug::debug("CalDAV Server: getCurrentUserPrincipal: no user returned from Auth Plugin");
			}
		}
		return $principal;
	}


	public function generateMultiStatus(array $fileProperties)
	{

		$dom = new DOMDocument('1.0','utf-8');
		$dom->formatOutput = true;
		$multiStatus = $dom->createElement('d:multistatus');
		$dom->appendChild($multiStatus);

		// Adding in default namespaces
		foreach($this->xmlNamespaces as $namespace=>$prefix)
		{
			$multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
		}

		foreach($fileProperties as $entry)
		{
			$href = $entry['href'];
			unset($entry['href']);

			$response = new Sabre_DAV_Property_Response($href,$entry);
			$response->serialize($this,$multiStatus);
		}

		return $dom->saveXML();
	}
}
